Update de proformas
This commit is contained in:
parent
88a7a36c4b
commit
ff6905b845
@ -1,3 +1,7 @@
|
|||||||
export const toSafeNumber = (value: number | null | undefined): number => {
|
const toSafeNumber = (value: number | null | undefined): number => {
|
||||||
return value ?? 0;
|
return value ?? 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NumberHelper = {
|
||||||
|
toSafeNumber,
|
||||||
|
};
|
||||||
|
|||||||
@ -230,8 +230,6 @@ export function useProformasGridColumns(
|
|||||||
const availableTransitions =
|
const availableTransitions =
|
||||||
PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? [];
|
PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? [];
|
||||||
|
|
||||||
console.log(availableTransitions, proforma.status);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{!isIssued && actionHandlers.onEditClick && (
|
{!isIssued && actionHandlers.onEditClick && (
|
||||||
|
|||||||
@ -68,7 +68,7 @@ export const GetProformaByIdAdapter = {
|
|||||||
const mapItem = (dto: GetProformaByIdResponseDTO["items"][number]): ProformaItem => {
|
const mapItem = (dto: GetProformaByIdResponseDTO["items"][number]): ProformaItem => {
|
||||||
return {
|
return {
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
position: dto.position,
|
position: Number(dto.position),
|
||||||
description: dto.description,
|
description: dto.description,
|
||||||
|
|
||||||
quantity: QuantityDTOHelper.toNumber(dto.quantity),
|
quantity: QuantityDTOHelper.toNumber(dto.quantity),
|
||||||
|
|||||||
@ -19,7 +19,6 @@ export const ProformaToListRowPatchAdapter = {
|
|||||||
fromProforma(proforma: Proforma): ProformaListRowPatch {
|
fromProforma(proforma: Proforma): ProformaListRowPatch {
|
||||||
return {
|
return {
|
||||||
id: proforma.id,
|
id: proforma.id,
|
||||||
customerId: proforma.customerId,
|
|
||||||
invoiceNumber: proforma.invoiceNumber,
|
invoiceNumber: proforma.invoiceNumber,
|
||||||
status: proforma.status,
|
status: proforma.status,
|
||||||
series: proforma.series,
|
series: proforma.series,
|
||||||
@ -29,10 +28,19 @@ export const ProformaToListRowPatchAdapter = {
|
|||||||
currencyCode: proforma.currencyCode,
|
currencyCode: proforma.currencyCode,
|
||||||
reference: proforma.reference,
|
reference: proforma.reference,
|
||||||
description: proforma.description,
|
description: proforma.description,
|
||||||
recipientName: proforma.recipient.name,
|
recipient: {
|
||||||
recipientTin: proforma.recipient.tin,
|
id: proforma.customerId,
|
||||||
|
tin: proforma.recipient.tin,
|
||||||
|
name: proforma.recipient.name,
|
||||||
|
street: proforma.recipient.street,
|
||||||
|
street2: proforma.recipient.street2,
|
||||||
|
city: proforma.recipient.city,
|
||||||
|
province: proforma.recipient.province,
|
||||||
|
postalCode: proforma.recipient.postalCode,
|
||||||
|
country: proforma.recipient.country,
|
||||||
|
},
|
||||||
subtotalAmount: proforma.subtotalAmount,
|
subtotalAmount: proforma.subtotalAmount,
|
||||||
discountPercentage: proforma.discountPercentage,
|
discountPercentage: proforma.globalDiscountPercentage,
|
||||||
discountAmount: proforma.discountAmount,
|
discountAmount: proforma.discountAmount,
|
||||||
taxableAmount: proforma.taxableAmount,
|
taxableAmount: proforma.taxableAmount,
|
||||||
taxesAmount: proforma.taxesAmount,
|
taxesAmount: proforma.taxesAmount,
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
export interface ProformaItem {
|
export interface ProformaItem {
|
||||||
id: string;
|
id: string;
|
||||||
position: string;
|
position: number;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
import type { ProformaItem } from "../../shared";
|
||||||
|
import type { ProformaItemUpdateForm } from "../entities";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapea un cliente a un formulario de actualización de cliente.
|
||||||
|
*
|
||||||
|
* @param proforma
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const mapProformaItemsToProformaItemsUpdateForm = (
|
||||||
|
item: ProformaItem
|
||||||
|
): ProformaItemUpdateForm => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
position: item.position,
|
||||||
|
description: item.description,
|
||||||
|
quantity: item.quantity,
|
||||||
|
unitAmount: item.unitAmount,
|
||||||
|
itemDiscountPercentage: item.itemDiscountPercentage,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import type { Proforma } from "../../shared";
|
import type { Proforma } from "../../shared";
|
||||||
import type { ProformaUpdateForm } from "../entities";
|
import type { ProformaUpdateForm } from "../entities";
|
||||||
|
|
||||||
|
import { mapProformaItemsToProformaItemsUpdateForm } from "./proforma-items-to-proforma-items-update-form.adapter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapea un cliente a un formulario de actualización de cliente.
|
* Mapea un cliente a un formulario de actualización de cliente.
|
||||||
*
|
*
|
||||||
@ -27,5 +29,7 @@ export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpd
|
|||||||
globalDiscountPercentage: proforma.globalDiscountPercentage ?? 0,
|
globalDiscountPercentage: proforma.globalDiscountPercentage ?? 0,
|
||||||
|
|
||||||
paymentMethod: proforma.paymentMethod ?? "",
|
paymentMethod: proforma.paymentMethod ?? "",
|
||||||
|
|
||||||
|
items: proforma.items.map(mapProformaItemsToProformaItemsUpdateForm),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,12 +9,9 @@ import type { UpdateProformaByIdParams } from "../../shared";
|
|||||||
import type { Proforma } from "../../shared/entities";
|
import type { Proforma } from "../../shared/entities";
|
||||||
import { useProformaGetQuery, useProformaUpdateMutation } from "../../shared/hooks";
|
import { useProformaGetQuery, useProformaUpdateMutation } from "../../shared/hooks";
|
||||||
import { mapProformaToProformaUpdateForm, mapProformaToSelectedCustomer } from "../adapters";
|
import { mapProformaToProformaUpdateForm, mapProformaToSelectedCustomer } from "../adapters";
|
||||||
|
import { type ProformaUpdateForm, ProformaUpdateFormSchema } from "../entities";
|
||||||
import {
|
import {
|
||||||
type ProformaUpdateForm,
|
buildProformaUpdateDefault,
|
||||||
ProformaUpdateFormSchema,
|
|
||||||
defaultProformaUpdateForm,
|
|
||||||
} from "../entities";
|
|
||||||
import {
|
|
||||||
buildProformaUpdatePatch,
|
buildProformaUpdatePatch,
|
||||||
buildUpdateProformaByIdParams,
|
buildUpdateProformaByIdParams,
|
||||||
focusFirstProformaUpdateError,
|
focusFirstProformaUpdateError,
|
||||||
@ -54,7 +51,7 @@ export const useUpdateProformaController = (
|
|||||||
} = useProformaUpdateMutation();
|
} = useProformaUpdateMutation();
|
||||||
|
|
||||||
const initialValues = useMemo<ProformaUpdateForm>(() => {
|
const initialValues = useMemo<ProformaUpdateForm>(() => {
|
||||||
if (!proformaData) return defaultProformaUpdateForm;
|
if (!proformaData) return buildProformaUpdateDefault();
|
||||||
|
|
||||||
return mapProformaToProformaUpdateForm(proformaData);
|
return mapProformaToProformaUpdateForm(proformaData);
|
||||||
}, [proformaData]);
|
}, [proformaData]);
|
||||||
@ -84,7 +81,7 @@ export const useUpdateProformaController = (
|
|||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
const initialData = proformaData
|
const initialData = proformaData
|
||||||
? mapProformaToProformaUpdateForm(proformaData)
|
? mapProformaToProformaUpdateForm(proformaData)
|
||||||
: defaultProformaUpdateForm;
|
: buildProformaUpdateDefault();
|
||||||
|
|
||||||
form.reset(initialData, { keepDirty: false });
|
form.reset(initialData, { keepDirty: false });
|
||||||
setSelectedCustomer(mapProformaToSelectedCustomer(proformaData));
|
setSelectedCustomer(mapProformaToSelectedCustomer(proformaData));
|
||||||
@ -120,6 +117,9 @@ export const useUpdateProformaController = (
|
|||||||
const previousData = proformaData;
|
const previousData = proformaData;
|
||||||
|
|
||||||
const patchData = buildProformaUpdatePatch(formData, form.formState.dirtyFields);
|
const patchData = buildProformaUpdatePatch(formData, form.formState.dirtyFields);
|
||||||
|
|
||||||
|
console.log(patchData);
|
||||||
|
|
||||||
const params = buildUpdateProformaByIdParams(proformaId, patchData);
|
const params = buildUpdateProformaByIdParams(proformaId, patchData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -147,7 +147,9 @@ export const useUpdateProformaController = (
|
|||||||
error instanceof Error ? error : new Error(t("pages.update.error.unknown"));
|
error instanceof Error ? error : new Error(t("pages.update.error.unknown"));
|
||||||
|
|
||||||
form.reset(
|
form.reset(
|
||||||
previousData ? mapProformaToProformaUpdateForm(previousData) : defaultProformaUpdateForm,
|
previousData
|
||||||
|
? mapProformaToProformaUpdateForm(previousData)
|
||||||
|
: buildProformaUpdateDefault(),
|
||||||
{ keepDirty: false }
|
{ keepDirty: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -159,6 +161,7 @@ export const useUpdateProformaController = (
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
(errors: FieldErrors<ProformaUpdateForm>) => {
|
(errors: FieldErrors<ProformaUpdateForm>) => {
|
||||||
|
console.log(errors);
|
||||||
focusFirstProformaUpdateError(errors);
|
focusFirstProformaUpdateError(errors);
|
||||||
|
|
||||||
showWarningToast(
|
showWarningToast(
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { NumberHelper } from "@erp/core";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
type FieldArrayWithId,
|
type FieldArrayWithId,
|
||||||
@ -7,11 +8,11 @@ import {
|
|||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
|
|
||||||
import type { ProformaItemUpdateForm, ProformaUpdateForm } from "../entities";
|
import type { ProformaItemUpdateForm, ProformaUpdateForm } from "../entities";
|
||||||
import { buildProformaItemUpdateDefault } from "../utils/build-proforma-item-update-default";
|
import { buildProformaItemUpdateDefault } from "../utils";
|
||||||
|
|
||||||
export interface ProformaItemAmounts {
|
export interface ProformaItemAmounts {
|
||||||
subtotal: number;
|
subtotal: number;
|
||||||
discountAmount: number;
|
itemDiscountAmount: number;
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,23 +57,27 @@ const normalizeItemPositions = (items: ProformaItemUpdateForm[]): ProformaItemUp
|
|||||||
};
|
};
|
||||||
|
|
||||||
const calculateItemAmounts = (
|
const calculateItemAmounts = (
|
||||||
item?: Pick<ProformaItemUpdateForm, "quantity" | "unitAmount" | "discountPercentage">
|
item?: Pick<ProformaItemUpdateForm, "quantity" | "unitAmount" | "itemDiscountPercentage">
|
||||||
): ProformaItemAmounts => {
|
): ProformaItemAmounts => {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return {
|
return {
|
||||||
subtotal: 0,
|
subtotal: 0,
|
||||||
discountAmount: 0,
|
itemDiscountAmount: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const subtotal = roundCurrency(item.quantity * item.unitAmount);
|
const quantity = NumberHelper.toSafeNumber(item.quantity);
|
||||||
const discountAmount = roundCurrency(subtotal * (item.discountPercentage / 100));
|
const unitAmount = NumberHelper.toSafeNumber(item.unitAmount);
|
||||||
const total = roundCurrency(subtotal - discountAmount);
|
const itemDiscountPercentage = NumberHelper.toSafeNumber(item.itemDiscountPercentage);
|
||||||
|
|
||||||
|
const subtotal = roundCurrency(quantity * unitAmount);
|
||||||
|
const itemDiscountAmount = roundCurrency(subtotal * (itemDiscountPercentage / 100));
|
||||||
|
const total = roundCurrency(subtotal - itemDiscountAmount);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subtotal,
|
subtotal,
|
||||||
discountAmount,
|
itemDiscountAmount,
|
||||||
total,
|
total,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -84,7 +89,7 @@ const calculateItemsTotals = (items: ProformaItemUpdateForm[]): ProformaItemsTot
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
subtotal: roundCurrency(acc.subtotal + amounts.subtotal),
|
subtotal: roundCurrency(acc.subtotal + amounts.subtotal),
|
||||||
discountAmount: roundCurrency(acc.discountAmount + amounts.discountAmount),
|
discountAmount: roundCurrency(acc.discountAmount + amounts.itemDiscountAmount),
|
||||||
total: roundCurrency(acc.total + amounts.total),
|
total: roundCurrency(acc.total + amounts.total),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export * from "./proforma-item-update-form.entity";
|
export * from "./proforma-item-update-form.entity";
|
||||||
|
export * from "./proforma-item-update-form.schema";
|
||||||
|
export * from "./proforma-item-update-patch.entity";
|
||||||
export * from "./proforma-update-form.entity";
|
export * from "./proforma-update-form.entity";
|
||||||
export * from "./proforma-update-form.schema";
|
export * from "./proforma-update-form.schema";
|
||||||
export * from "./proforma-update-form-default.entity";
|
|
||||||
export * from "./proforma-update-patch.entity";
|
export * from "./proforma-update-patch.entity";
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export interface ProformaItemUpdateForm {
|
|||||||
id: string;
|
id: string;
|
||||||
position: number;
|
position: number;
|
||||||
description: string;
|
description: string;
|
||||||
quantity: number;
|
quantity: number | null;
|
||||||
unitAmount: number;
|
unitAmount: number | null;
|
||||||
discountPercentage: number;
|
itemDiscountPercentage: number | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,10 +17,11 @@ import { z } from "zod/v4";
|
|||||||
export const ProformaItemUpdateFormSchema = z.object({
|
export const ProformaItemUpdateFormSchema = z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
position: z.number().int().nonnegative(),
|
position: z.number().int().nonnegative(),
|
||||||
|
|
||||||
description: z.string().trim(),
|
description: z.string().trim(),
|
||||||
quantity: z.number().positive(),
|
quantity: z.number().positive().nullable(),
|
||||||
unitAmount: z.number().nonnegative(),
|
unitAmount: z.number().nonnegative().nullable(),
|
||||||
discountPercentage: z.number().min(0).max(100),
|
itemDiscountPercentage: z.number().min(0).max(100).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ProformaItemUpdateForm = z.infer<typeof ProformaItemUpdateFormSchema>;
|
export type ProformaItemUpdateFormSchemaType = z.infer<typeof ProformaItemUpdateFormSchema>;
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
export interface ProformaItemUpdatePatch {
|
||||||
|
id: string;
|
||||||
|
position: number;
|
||||||
|
description: string;
|
||||||
|
quantity: number | null;
|
||||||
|
unitAmount: number | null;
|
||||||
|
itemDiscountPercentage: number | null;
|
||||||
|
}
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import type { ProformaUpdateForm } from ".";
|
|
||||||
|
|
||||||
export const defaultProformaUpdateForm: ProformaUpdateForm = {
|
|
||||||
series: "",
|
|
||||||
|
|
||||||
invoiceDate: "",
|
|
||||||
operationDate: "",
|
|
||||||
|
|
||||||
customerId: "",
|
|
||||||
|
|
||||||
description: "",
|
|
||||||
reference: "",
|
|
||||||
notes: "",
|
|
||||||
|
|
||||||
languageCode: "es",
|
|
||||||
currencyCode: "EUR",
|
|
||||||
|
|
||||||
paymentMethod: "",
|
|
||||||
|
|
||||||
globalDiscountPercentage: 0,
|
|
||||||
|
|
||||||
items: [],
|
|
||||||
};
|
|
||||||
@ -13,7 +13,7 @@
|
|||||||
* - sin detalles impuestos por el widget
|
* - sin detalles impuestos por el widget
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ProformaItemUpdateForm } from "./proforma-item-update-form.schema";
|
import type { ProformaItemUpdateForm } from "./proforma-item-update-form.entity";
|
||||||
|
|
||||||
export interface ProformaUpdateForm {
|
export interface ProformaUpdateForm {
|
||||||
series: string;
|
series: string;
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="space-y-6" id={formId} onSubmit={onSubmit}>
|
<form className="space-y-6" id={formId} noValidate onSubmit={onSubmit}>
|
||||||
<ProformaUpdateHeaderEditor disabled={isSubmitting} />
|
<ProformaUpdateHeaderEditor disabled={isSubmitting} />
|
||||||
|
|
||||||
<ProformaUpdateRecipientEditor
|
<ProformaUpdateRecipientEditor
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { TextField } from "@repo/rdx-ui/components";
|
import { AmountField, PercentageField, QuantityField, TextField } from "@repo/rdx-ui/components";
|
||||||
import { Button, Card, CardContent } from "@repo/shadcn-ui/components";
|
import { Button, Card, CardContent } from "@repo/shadcn-ui/components";
|
||||||
import { CopyIcon, MoveDownIcon, MoveUpIcon, Trash2Icon } from "lucide-react";
|
import { CopyIcon, MoveDownIcon, MoveUpIcon, Trash2Icon } from "lucide-react";
|
||||||
|
|
||||||
@ -38,28 +38,24 @@ export const ProformaUpdateItemRowEditor = ({
|
|||||||
className="col-span-12"
|
className="col-span-12"
|
||||||
label={t("form_fields.items.description.label", "Descripción")}
|
label={t("form_fields.items.description.label", "Descripción")}
|
||||||
name={`items.${index}.description`}
|
name={`items.${index}.description`}
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<QuantityField
|
||||||
className="col-span-12 md:col-span-3"
|
className="col-span-12 md:col-span-3"
|
||||||
label={t("form_fields.items.quantity.label", "Cantidad")}
|
label={t("form_fields.items.quantity.label", "Cantidad")}
|
||||||
name={`items.${index}.quantity`}
|
name={`items.${index}.quantity`}
|
||||||
typePreset="number"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<AmountField
|
||||||
className="col-span-12 md:col-span-3"
|
className="col-span-12 md:col-span-3"
|
||||||
label={t("form_fields.items.unit_amount.label", "Importe unitario")}
|
label={t("form_fields.items.unit_amount.label", "Importe unitario")}
|
||||||
name={`items.${index}.unitAmount`}
|
name={`items.${index}.unitAmount`}
|
||||||
typePreset="number"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<PercentageField
|
||||||
className="col-span-12 md:col-span-3"
|
className="col-span-12 md:col-span-3"
|
||||||
label={t("form_fields.items.discount_percentage.label", "Descuento %")}
|
label={t("form_fields.items.discount_percentage.label", "Descuento %")}
|
||||||
name={`items.${index}.discountPercentage`}
|
name={`items.${index}.discountPercentage`}
|
||||||
typePreset="number"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="col-span-12 md:col-span-3 rounded-md border p-3 text-sm">
|
<div className="col-span-12 md:col-span-3 rounded-md border p-3 text-sm">
|
||||||
@ -70,7 +66,7 @@ export const ProformaUpdateItemRowEditor = ({
|
|||||||
|
|
||||||
<div className="flex justify-between gap-3">
|
<div className="flex justify-between gap-3">
|
||||||
<span>{t("form_fields.items.discount_amount.label", "Descuento")}</span>
|
<span>{t("form_fields.items.discount_amount.label", "Descuento")}</span>
|
||||||
<span>{amounts.discountAmount}</span>
|
<span>{amounts.itemDiscountAmount}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between gap-3 font-medium">
|
<div className="flex justify-between gap-3 font-medium">
|
||||||
|
|||||||
@ -7,8 +7,8 @@ export const buildProformaItemUpdateDefault = (position: number): ProformaItemUp
|
|||||||
id: UniqueID.generateNewID().toString(),
|
id: UniqueID.generateNewID().toString(),
|
||||||
position,
|
position,
|
||||||
description: "",
|
description: "",
|
||||||
quantity: 1,
|
quantity: null,
|
||||||
unitAmount: 0,
|
unitAmount: null,
|
||||||
discountPercentage: 0,
|
itemDiscountPercentage: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import type { ProformaItemUpdateForm, ProformaItemUpdatePatch } from "../entities";
|
||||||
|
|
||||||
|
export const buildProformaItemsUpdatePatch = (
|
||||||
|
items: ProformaItemUpdateForm[]
|
||||||
|
): ProformaItemUpdatePatch[] => {
|
||||||
|
return items.map((item, index) => ({
|
||||||
|
id: item.id,
|
||||||
|
position: index,
|
||||||
|
description: item.description.trim(),
|
||||||
|
quantity: item.quantity,
|
||||||
|
unitAmount: item.unitAmount,
|
||||||
|
itemDiscountPercentage: item.itemDiscountPercentage,
|
||||||
|
}));
|
||||||
|
};
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import type { ProformaUpdateForm } from "../entities";
|
||||||
|
|
||||||
|
export const buildProformaUpdateDefault = (): ProformaUpdateForm => {
|
||||||
|
return {
|
||||||
|
series: "",
|
||||||
|
|
||||||
|
invoiceDate: "",
|
||||||
|
operationDate: "",
|
||||||
|
|
||||||
|
customerId: "",
|
||||||
|
|
||||||
|
description: "",
|
||||||
|
reference: "",
|
||||||
|
notes: "",
|
||||||
|
|
||||||
|
languageCode: "es",
|
||||||
|
currencyCode: "EUR",
|
||||||
|
|
||||||
|
paymentMethod: "",
|
||||||
|
|
||||||
|
globalDiscountPercentage: 0,
|
||||||
|
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -3,6 +3,8 @@ import type { FieldNamesMarkedBoolean } from "react-hook-form";
|
|||||||
|
|
||||||
import type { ProformaUpdateForm, ProformaUpdatePatch } from "../entities";
|
import type { ProformaUpdateForm, ProformaUpdatePatch } from "../entities";
|
||||||
|
|
||||||
|
import { buildProformaItemsUpdatePatch } from "./build-proforma-items-update-patch";
|
||||||
|
|
||||||
export const buildProformaUpdatePatch = (
|
export const buildProformaUpdatePatch = (
|
||||||
formData: ProformaUpdateForm,
|
formData: ProformaUpdateForm,
|
||||||
dirtyFields: FieldNamesMarkedBoolean<ProformaUpdateForm>
|
dirtyFields: FieldNamesMarkedBoolean<ProformaUpdateForm>
|
||||||
@ -11,5 +13,10 @@ export const buildProformaUpdatePatch = (
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return pickFormDirtyValues(formData, dirtyFields) as ProformaUpdatePatch;
|
const itemsPatch = buildProformaItemsUpdatePatch(formData.items);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pickFormDirtyValues(formData, dirtyFields),
|
||||||
|
items: itemsPatch,
|
||||||
|
} as ProformaUpdatePatch;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./build-proforma-item-update-default";
|
export * from "./build-proforma-item-update-default";
|
||||||
|
export * from "./build-proforma-update-default";
|
||||||
export * from "./build-proforma-update-patch";
|
export * from "./build-proforma-update-patch";
|
||||||
export * from "./build-update-proforma-by-id-params";
|
export * from "./build-update-proforma-by-id-params";
|
||||||
export * from "./focus-first-proforma-update-error";
|
export * from "./focus-first-proforma-update-error";
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export const CustomerUpdateEditorForm = ({
|
|||||||
className,
|
className,
|
||||||
}: CustomerUpdateEditorFormProps) => {
|
}: CustomerUpdateEditorFormProps) => {
|
||||||
return (
|
return (
|
||||||
<form id={formId} noValidate onSubmit={onSubmit}>
|
<form className="space-y-6" id={formId} noValidate onSubmit={onSubmit}>
|
||||||
<section className={cn("space-y-12 p-6", className)}>
|
<section className={cn("space-y-12 p-6", className)}>
|
||||||
<CustomerBasicInfoFields />
|
<CustomerBasicInfoFields />
|
||||||
<CustomerAddressFields />
|
<CustomerAddressFields />
|
||||||
|
|||||||
@ -0,0 +1,180 @@
|
|||||||
|
import {
|
||||||
|
Field,
|
||||||
|
FieldDescription,
|
||||||
|
FieldError,
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
InputGroupInput,
|
||||||
|
} from "@repo/shadcn-ui/components";
|
||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
import * as React from "react";
|
||||||
|
import { type FieldPath, type FieldValues, useController, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { FormFieldLabel } from "../form-field-label.tsx";
|
||||||
|
|
||||||
|
import type { DecimalFieldBaseProps } from "./decimal-field.types.ts";
|
||||||
|
import {
|
||||||
|
DECIMAL_INPUT_PATTERN,
|
||||||
|
clampNumber,
|
||||||
|
formatDecimalValue,
|
||||||
|
parseDecimalOrNull,
|
||||||
|
trimToScale,
|
||||||
|
} from "./decimal-field.utils.ts";
|
||||||
|
|
||||||
|
type DecimalFieldProps<TFormValues extends FieldValues> = DecimalFieldBaseProps & {
|
||||||
|
name: FieldPath<TFormValues>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DecimalField = <TFormValues extends FieldValues>({
|
||||||
|
name,
|
||||||
|
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
|
||||||
|
required = false,
|
||||||
|
readOnly = false,
|
||||||
|
|
||||||
|
orientation = "vertical",
|
||||||
|
|
||||||
|
className,
|
||||||
|
inputClassName,
|
||||||
|
|
||||||
|
leftIcon,
|
||||||
|
rightIcon,
|
||||||
|
leftAddon,
|
||||||
|
rightAddon,
|
||||||
|
|
||||||
|
scale = 4,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
|
||||||
|
...inputRest
|
||||||
|
}: DecimalFieldProps<TFormValues>) => {
|
||||||
|
const { control, formState, getFieldState } = useFormContext<TFormValues>();
|
||||||
|
|
||||||
|
const { field } = useController({
|
||||||
|
name,
|
||||||
|
control,
|
||||||
|
defaultValue: null as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputId = React.useId();
|
||||||
|
const disabled = formState.isSubmitting || inputRest.disabled;
|
||||||
|
const fieldError = getFieldState(name, formState).error;
|
||||||
|
const [inputValue, setInputValue] = React.useState<string>(() =>
|
||||||
|
formatDecimalValue(field.value as number | null | undefined, scale)
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const nextFormattedValue = formatDecimalValue(field.value as number | null | undefined, scale);
|
||||||
|
|
||||||
|
setInputValue((currentValue) =>
|
||||||
|
currentValue === nextFormattedValue ? currentValue : nextFormattedValue
|
||||||
|
);
|
||||||
|
}, [field.value, scale]);
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const rawValue = event.target.value;
|
||||||
|
|
||||||
|
if (rawValue === "") {
|
||||||
|
setInputValue("");
|
||||||
|
field.onChange(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DECIMAL_INPUT_PATTERN.test(rawValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedValue = trimToScale(rawValue, scale);
|
||||||
|
setInputValue(trimmedValue);
|
||||||
|
|
||||||
|
const parsedValue = parseDecimalOrNull(trimmedValue);
|
||||||
|
|
||||||
|
if (parsedValue === null) {
|
||||||
|
field.onChange(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
field.onChange(clampNumber(parsedValue, min, max));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
field.onBlur();
|
||||||
|
|
||||||
|
const parsedValue = parseDecimalOrNull(event.target.value);
|
||||||
|
|
||||||
|
if (parsedValue === null) {
|
||||||
|
setInputValue("");
|
||||||
|
field.onChange(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clampedValue = clampNumber(parsedValue, min, max);
|
||||||
|
const formattedValue = formatDecimalValue(clampedValue, scale);
|
||||||
|
|
||||||
|
setInputValue(formattedValue);
|
||||||
|
field.onChange(clampedValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderedLeftAddon = leftAddon ?? leftIcon;
|
||||||
|
const renderedRightAddon = rightAddon ?? rightIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Field className={cn("gap-1", className)} data-invalid={!!fieldError} orientation={orientation}>
|
||||||
|
{label ? (
|
||||||
|
<FormFieldLabel htmlFor={inputId} required={required}>
|
||||||
|
{label}
|
||||||
|
</FormFieldLabel>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<>
|
||||||
|
<InputGroup
|
||||||
|
className={cn(
|
||||||
|
"bg-muted/50 font-medium transition",
|
||||||
|
"hover:border-ring hover:ring-ring/20 hover:ring-[3px]",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/60 focus-visible:ring-[3px]",
|
||||||
|
"placeholder:text-muted-foreground/50",
|
||||||
|
inputClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderedLeftAddon ? (
|
||||||
|
<InputGroupAddon aria-hidden="true" className="bg-muted/50 font-medium">
|
||||||
|
{renderedLeftAddon}
|
||||||
|
</InputGroupAddon>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<InputGroupInput
|
||||||
|
{...inputRest}
|
||||||
|
aria-invalid={!!fieldError}
|
||||||
|
autoComplete="off"
|
||||||
|
className="placeholder:text-muted-foreground/50"
|
||||||
|
disabled={disabled}
|
||||||
|
id={inputId}
|
||||||
|
inputMode="decimal"
|
||||||
|
name={field.name}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
readOnly={readOnly}
|
||||||
|
ref={field.ref}
|
||||||
|
required={required}
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{renderedRightAddon ? (
|
||||||
|
<InputGroupAddon aria-hidden="true">{renderedRightAddon}</InputGroupAddon>
|
||||||
|
) : null}
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
{description ? (
|
||||||
|
<FieldDescription>{description}</FieldDescription>
|
||||||
|
) : (
|
||||||
|
<div aria-hidden="true" className="min-h-5" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FieldError errors={[fieldError]} />
|
||||||
|
</>
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import type * as React from "react";
|
||||||
|
|
||||||
|
import type { NativeInputProps } from "../types.ts";
|
||||||
|
|
||||||
|
export type DecimalFieldOrientation = "vertical" | "horizontal" | "responsive";
|
||||||
|
|
||||||
|
export type DecimalFieldBaseProps = Omit<
|
||||||
|
NativeInputProps,
|
||||||
|
"name" | "type" | "value" | "defaultValue" | "onChange"
|
||||||
|
> & {
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
required?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
|
||||||
|
orientation?: DecimalFieldOrientation;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
inputClassName?: string;
|
||||||
|
|
||||||
|
leftIcon?: React.ReactNode;
|
||||||
|
rightIcon?: React.ReactNode;
|
||||||
|
|
||||||
|
leftAddon?: React.ReactNode;
|
||||||
|
rightAddon?: React.ReactNode;
|
||||||
|
|
||||||
|
scale?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
};
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
export const DECIMAL_INPUT_PATTERN = /^-?\d*([.,]\d*)?$/;
|
||||||
|
|
||||||
|
export const normalizeDecimalInput = (value: string): string => {
|
||||||
|
return value.replace(",", ".");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trimToScale = (value: string, scale: number): string => {
|
||||||
|
const normalized = normalizeDecimalInput(value);
|
||||||
|
|
||||||
|
if (!normalized.includes(".")) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [integerPart, decimalPart = ""] = normalized.split(".");
|
||||||
|
|
||||||
|
return `${integerPart}.${decimalPart.slice(0, scale)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseDecimalOrNull = (value: string): number | null => {
|
||||||
|
const normalized = normalizeDecimalInput(value).trim();
|
||||||
|
|
||||||
|
if (normalized === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === "-" || normalized === "." || normalized === "-.") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(normalized);
|
||||||
|
|
||||||
|
if (!Number.isFinite(parsed)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clampNumber = (value: number, min?: number, max?: number): number => {
|
||||||
|
if (typeof min === "number" && value < min) {
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof max === "number" && value > max) {
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDecimalValue = (value: number | null | undefined, scale: number): string => {
|
||||||
|
if (value === null || value === undefined || Number.isNaN(value)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const asString = String(value);
|
||||||
|
|
||||||
|
if (!asString.includes(".")) {
|
||||||
|
return asString;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [integerPart, decimalPart = ""] = asString.split(".");
|
||||||
|
const trimmedDecimalPart = decimalPart.slice(0, scale).replace(/0+$/, "");
|
||||||
|
|
||||||
|
return trimmedDecimalPart.length > 0 ? `${integerPart}.${trimmedDecimalPart}` : `${integerPart}`;
|
||||||
|
};
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./decimal-field.tsx";
|
||||||
|
export * from "./decimal-field.types.ts";
|
||||||
@ -1,8 +1,10 @@
|
|||||||
export * from "./date-picker-field.tsx";
|
export * from "./date-picker-field.tsx";
|
||||||
export * from "./date-picker-input-field/index.ts";
|
export * from "./date-picker-input-field/index.ts";
|
||||||
|
export * from "./decimal-field/index.ts";
|
||||||
export * from "./form-field-label.tsx";
|
export * from "./form-field-label.tsx";
|
||||||
export * from "./multi-select-field.tsx";
|
export * from "./multi-select-field.tsx";
|
||||||
export * from "./radio-group-field.tsx";
|
export * from "./radio-group-field.tsx";
|
||||||
export * from "./select-field.tsx";
|
export * from "./select-field.tsx";
|
||||||
|
export * from "./semantic-numeric-fields/index.ts";
|
||||||
export * from "./text-area-field.tsx";
|
export * from "./text-area-field.tsx";
|
||||||
export * from "./text-field.tsx";
|
export * from "./text-field.tsx";
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
// packages/rdx-ui/src/components/form/amount-field.tsx
|
||||||
|
import type { FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
|
import { DecimalField } from "../decimal-field/index.ts";
|
||||||
|
|
||||||
|
import type { SemanticNumericFieldProps } from "./semantic-numeric-fields.types.ts";
|
||||||
|
|
||||||
|
export const AmountField = <TFormValues extends FieldValues>({
|
||||||
|
scale = 4,
|
||||||
|
rightAddon = "€",
|
||||||
|
...props
|
||||||
|
}: SemanticNumericFieldProps<TFormValues>) => {
|
||||||
|
return <DecimalField {...props} min={0} rightAddon={rightAddon} scale={scale} />;
|
||||||
|
};
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./amount-field.tsx";
|
||||||
|
export * from "./percentage-field.tsx";
|
||||||
|
export * from "./quantity-field.tsx";
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
// packages/rdx-ui/src/components/form/percentage-field.tsx
|
||||||
|
import type { FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
|
import { DecimalField } from "../decimal-field/index.ts";
|
||||||
|
|
||||||
|
import type { SemanticNumericFieldProps } from "./semantic-numeric-fields.types.ts";
|
||||||
|
|
||||||
|
export const PercentageField = <TFormValues extends FieldValues>({
|
||||||
|
scale = 4,
|
||||||
|
rightAddon = "%",
|
||||||
|
...props
|
||||||
|
}: SemanticNumericFieldProps<TFormValues>) => {
|
||||||
|
return <DecimalField {...props} max={100} min={0} rightAddon={rightAddon} scale={scale} />;
|
||||||
|
};
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import type { FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
|
import { DecimalField } from "../decimal-field/index.ts";
|
||||||
|
|
||||||
|
import type { SemanticNumericFieldProps } from "./semantic-numeric-fields.types.ts";
|
||||||
|
|
||||||
|
export const QuantityField = <TFormValues extends FieldValues>({
|
||||||
|
scale = 4,
|
||||||
|
...props
|
||||||
|
}: SemanticNumericFieldProps<TFormValues>) => {
|
||||||
|
return <DecimalField {...props} scale={scale} />;
|
||||||
|
};
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
// packages/rdx-ui/src/components/form/semantic-numeric-fields.types.ts
|
||||||
|
import type { FieldPath, FieldValues } from "react-hook-form";
|
||||||
|
|
||||||
|
import type { DecimalFieldBaseProps } from "../decimal-field/index.ts";
|
||||||
|
|
||||||
|
export type SemanticNumericFieldProps<TFormValues extends FieldValues> = Omit<
|
||||||
|
DecimalFieldBaseProps,
|
||||||
|
"min" | "max"
|
||||||
|
> & {
|
||||||
|
name: FieldPath<TFormValues>;
|
||||||
|
};
|
||||||
@ -63,7 +63,7 @@ export const getInputPresetProps = (preset: TextFieldTypePreset = "text"): Resol
|
|||||||
|
|
||||||
case "number":
|
case "number":
|
||||||
return {
|
return {
|
||||||
type: "text",
|
type: "number",
|
||||||
inputMode: "numeric",
|
inputMode: "numeric",
|
||||||
autoComplete: "off",
|
autoComplete: "off",
|
||||||
spellCheck: false,
|
spellCheck: false,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user