Compare commits
No commits in common. "ff6905b845616d8c9cdd3abe8036002847c34ba9" and "0caf747d4ec594e664ea02363338392cb77c9857" have entirely different histories.
ff6905b845
...
0caf747d4e
@ -2,6 +2,5 @@ export * from "./date-helper";
|
|||||||
export * from "./dto-compare-helper";
|
export * from "./dto-compare-helper";
|
||||||
export * from "./money-dto-helper";
|
export * from "./money-dto-helper";
|
||||||
export * from "./money-helper";
|
export * from "./money-helper";
|
||||||
export * from "./number-helper";
|
export * from "./percentage-dto-helpers";
|
||||||
export * from "./percentage-dto-helper";
|
export * from "./quantity-dto-helpers";
|
||||||
export * from "./quantity-dto-helper";
|
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
const toSafeNumber = (value: number | null | undefined): number => {
|
|
||||||
return value ?? 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NumberHelper = {
|
|
||||||
toSafeNumber,
|
|
||||||
};
|
|
||||||
@ -230,6 +230,8 @@ 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: Number(dto.position),
|
position: dto.position,
|
||||||
description: dto.description,
|
description: dto.description,
|
||||||
|
|
||||||
quantity: QuantityDTOHelper.toNumber(dto.quantity),
|
quantity: QuantityDTOHelper.toNumber(dto.quantity),
|
||||||
|
|||||||
@ -19,6 +19,7 @@ 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,
|
||||||
@ -28,19 +29,10 @@ export const ProformaToListRowPatchAdapter = {
|
|||||||
currencyCode: proforma.currencyCode,
|
currencyCode: proforma.currencyCode,
|
||||||
reference: proforma.reference,
|
reference: proforma.reference,
|
||||||
description: proforma.description,
|
description: proforma.description,
|
||||||
recipient: {
|
recipientName: proforma.recipient.name,
|
||||||
id: proforma.customerId,
|
recipientTin: proforma.recipient.tin,
|
||||||
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.globalDiscountPercentage,
|
discountPercentage: proforma.discountPercentage,
|
||||||
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: number;
|
position: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
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,8 +1,6 @@
|
|||||||
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.
|
||||||
*
|
*
|
||||||
@ -29,7 +27,5 @@ 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,9 +9,12 @@ 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 {
|
||||||
buildProformaUpdateDefault,
|
type ProformaUpdateForm,
|
||||||
|
ProformaUpdateFormSchema,
|
||||||
|
defaultProformaUpdateForm,
|
||||||
|
} from "../entities";
|
||||||
|
import {
|
||||||
buildProformaUpdatePatch,
|
buildProformaUpdatePatch,
|
||||||
buildUpdateProformaByIdParams,
|
buildUpdateProformaByIdParams,
|
||||||
focusFirstProformaUpdateError,
|
focusFirstProformaUpdateError,
|
||||||
@ -51,7 +54,7 @@ export const useUpdateProformaController = (
|
|||||||
} = useProformaUpdateMutation();
|
} = useProformaUpdateMutation();
|
||||||
|
|
||||||
const initialValues = useMemo<ProformaUpdateForm>(() => {
|
const initialValues = useMemo<ProformaUpdateForm>(() => {
|
||||||
if (!proformaData) return buildProformaUpdateDefault();
|
if (!proformaData) return defaultProformaUpdateForm;
|
||||||
|
|
||||||
return mapProformaToProformaUpdateForm(proformaData);
|
return mapProformaToProformaUpdateForm(proformaData);
|
||||||
}, [proformaData]);
|
}, [proformaData]);
|
||||||
@ -81,7 +84,7 @@ export const useUpdateProformaController = (
|
|||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
const initialData = proformaData
|
const initialData = proformaData
|
||||||
? mapProformaToProformaUpdateForm(proformaData)
|
? mapProformaToProformaUpdateForm(proformaData)
|
||||||
: buildProformaUpdateDefault();
|
: defaultProformaUpdateForm;
|
||||||
|
|
||||||
form.reset(initialData, { keepDirty: false });
|
form.reset(initialData, { keepDirty: false });
|
||||||
setSelectedCustomer(mapProformaToSelectedCustomer(proformaData));
|
setSelectedCustomer(mapProformaToSelectedCustomer(proformaData));
|
||||||
@ -117,9 +120,6 @@ 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,9 +147,7 @@ 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
|
previousData ? mapProformaToProformaUpdateForm(previousData) : defaultProformaUpdateForm,
|
||||||
? mapProformaToProformaUpdateForm(previousData)
|
|
||||||
: buildProformaUpdateDefault(),
|
|
||||||
{ keepDirty: false }
|
{ keepDirty: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -161,7 +159,6 @@ export const useUpdateProformaController = (
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
(errors: FieldErrors<ProformaUpdateForm>) => {
|
(errors: FieldErrors<ProformaUpdateForm>) => {
|
||||||
console.log(errors);
|
|
||||||
focusFirstProformaUpdateError(errors);
|
focusFirstProformaUpdateError(errors);
|
||||||
|
|
||||||
showWarningToast(
|
showWarningToast(
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { NumberHelper } from "@erp/core";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
type FieldArrayWithId,
|
type FieldArrayWithId,
|
||||||
@ -8,11 +7,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";
|
import { buildProformaItemUpdateDefault } from "../utils/build-proforma-item-update-default";
|
||||||
|
|
||||||
export interface ProformaItemAmounts {
|
export interface ProformaItemAmounts {
|
||||||
subtotal: number;
|
subtotal: number;
|
||||||
itemDiscountAmount: number;
|
discountAmount: number;
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,27 +56,23 @@ const normalizeItemPositions = (items: ProformaItemUpdateForm[]): ProformaItemUp
|
|||||||
};
|
};
|
||||||
|
|
||||||
const calculateItemAmounts = (
|
const calculateItemAmounts = (
|
||||||
item?: Pick<ProformaItemUpdateForm, "quantity" | "unitAmount" | "itemDiscountPercentage">
|
item?: Pick<ProformaItemUpdateForm, "quantity" | "unitAmount" | "discountPercentage">
|
||||||
): ProformaItemAmounts => {
|
): ProformaItemAmounts => {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return {
|
return {
|
||||||
subtotal: 0,
|
subtotal: 0,
|
||||||
itemDiscountAmount: 0,
|
discountAmount: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const quantity = NumberHelper.toSafeNumber(item.quantity);
|
const subtotal = roundCurrency(item.quantity * item.unitAmount);
|
||||||
const unitAmount = NumberHelper.toSafeNumber(item.unitAmount);
|
const discountAmount = roundCurrency(subtotal * (item.discountPercentage / 100));
|
||||||
const itemDiscountPercentage = NumberHelper.toSafeNumber(item.itemDiscountPercentage);
|
const total = roundCurrency(subtotal - discountAmount);
|
||||||
|
|
||||||
const subtotal = roundCurrency(quantity * unitAmount);
|
|
||||||
const itemDiscountAmount = roundCurrency(subtotal * (itemDiscountPercentage / 100));
|
|
||||||
const total = roundCurrency(subtotal - itemDiscountAmount);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subtotal,
|
subtotal,
|
||||||
itemDiscountAmount,
|
discountAmount,
|
||||||
total,
|
total,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -89,7 +84,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.itemDiscountAmount),
|
discountAmount: roundCurrency(acc.discountAmount + amounts.discountAmount),
|
||||||
total: roundCurrency(acc.total + amounts.total),
|
total: roundCurrency(acc.total + amounts.total),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
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 | null;
|
quantity: number;
|
||||||
unitAmount: number | null;
|
unitAmount: number;
|
||||||
itemDiscountPercentage: number | null;
|
discountPercentage: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,11 +17,10 @@ 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().nullable(),
|
quantity: z.number().positive(),
|
||||||
unitAmount: z.number().nonnegative().nullable(),
|
unitAmount: z.number().nonnegative(),
|
||||||
itemDiscountPercentage: z.number().min(0).max(100).nullable(),
|
discountPercentage: z.number().min(0).max(100),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ProformaItemUpdateFormSchemaType = z.infer<typeof ProformaItemUpdateFormSchema>;
|
export type ProformaItemUpdateForm = z.infer<typeof ProformaItemUpdateFormSchema>;
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
export interface ProformaItemUpdatePatch {
|
|
||||||
id: string;
|
|
||||||
position: number;
|
|
||||||
description: string;
|
|
||||||
quantity: number | null;
|
|
||||||
unitAmount: number | null;
|
|
||||||
itemDiscountPercentage: number | null;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
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.entity";
|
import type { ProformaItemUpdateForm } from "./proforma-item-update-form.schema";
|
||||||
|
|
||||||
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} noValidate onSubmit={onSubmit}>
|
<form className="space-y-6" id={formId} onSubmit={onSubmit}>
|
||||||
<ProformaUpdateHeaderEditor disabled={isSubmitting} />
|
<ProformaUpdateHeaderEditor disabled={isSubmitting} />
|
||||||
|
|
||||||
<ProformaUpdateRecipientEditor
|
<ProformaUpdateRecipientEditor
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { AmountField, PercentageField, QuantityField, TextField } from "@repo/rdx-ui/components";
|
import { 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,24 +38,28 @@ 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
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<QuantityField
|
<TextField
|
||||||
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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AmountField
|
<TextField
|
||||||
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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PercentageField
|
<TextField
|
||||||
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">
|
||||||
@ -66,7 +70,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.itemDiscountAmount}</span>
|
<span>{amounts.discountAmount}</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: null,
|
quantity: 1,
|
||||||
unitAmount: null,
|
unitAmount: 0,
|
||||||
itemDiscountPercentage: null,
|
discountPercentage: 0,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
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,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
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,8 +3,6 @@ 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>
|
||||||
@ -13,10 +11,5 @@ export const buildProformaUpdatePatch = (
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemsPatch = buildProformaItemsUpdatePatch(formData.items);
|
return pickFormDirtyValues(formData, dirtyFields) as ProformaUpdatePatch;
|
||||||
|
|
||||||
return {
|
|
||||||
...pickFormDirtyValues(formData, dirtyFields),
|
|
||||||
items: itemsPatch,
|
|
||||||
} as ProformaUpdatePatch;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
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 className="space-y-6" id={formId} noValidate onSubmit={onSubmit}>
|
<form 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 />
|
||||||
|
|||||||
@ -1,180 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
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}`;
|
|
||||||
};
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./decimal-field.tsx";
|
|
||||||
export * from "./decimal-field.types.ts";
|
|
||||||
@ -1,10 +1,8 @@
|
|||||||
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";
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
// 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} />;
|
|
||||||
};
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from "./amount-field.tsx";
|
|
||||||
export * from "./percentage-field.tsx";
|
|
||||||
export * from "./quantity-field.tsx";
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
// 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} />;
|
|
||||||
};
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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} />;
|
|
||||||
};
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
// 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: "number",
|
type: "text",
|
||||||
inputMode: "numeric",
|
inputMode: "numeric",
|
||||||
autoComplete: "off",
|
autoComplete: "off",
|
||||||
spellCheck: false,
|
spellCheck: false,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user