Update de proformas

This commit is contained in:
David Arranz 2026-04-13 13:05:00 +02:00
parent 88a7a36c4b
commit ff6905b845
34 changed files with 483 additions and 74 deletions

View File

@ -1,3 +1,7 @@
export const toSafeNumber = (value: number | null | undefined): number => {
const toSafeNumber = (value: number | null | undefined): number => {
return value ?? 0;
};
export const NumberHelper = {
toSafeNumber,
};

View File

@ -230,8 +230,6 @@ export function useProformasGridColumns(
const availableTransitions =
PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? [];
console.log(availableTransitions, proforma.status);
return (
<div className="flex items-center gap-1">
{!isIssued && actionHandlers.onEditClick && (

View File

@ -68,7 +68,7 @@ export const GetProformaByIdAdapter = {
const mapItem = (dto: GetProformaByIdResponseDTO["items"][number]): ProformaItem => {
return {
id: dto.id,
position: dto.position,
position: Number(dto.position),
description: dto.description,
quantity: QuantityDTOHelper.toNumber(dto.quantity),

View File

@ -19,7 +19,6 @@ export const ProformaToListRowPatchAdapter = {
fromProforma(proforma: Proforma): ProformaListRowPatch {
return {
id: proforma.id,
customerId: proforma.customerId,
invoiceNumber: proforma.invoiceNumber,
status: proforma.status,
series: proforma.series,
@ -29,10 +28,19 @@ export const ProformaToListRowPatchAdapter = {
currencyCode: proforma.currencyCode,
reference: proforma.reference,
description: proforma.description,
recipientName: proforma.recipient.name,
recipientTin: proforma.recipient.tin,
recipient: {
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,
discountPercentage: proforma.discountPercentage,
discountPercentage: proforma.globalDiscountPercentage,
discountAmount: proforma.discountAmount,
taxableAmount: proforma.taxableAmount,
taxesAmount: proforma.taxesAmount,

View File

@ -6,7 +6,7 @@
export interface ProformaItem {
id: string;
position: string;
position: number;
description: string;
quantity: number;

View File

@ -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,
};
};

View File

@ -1,6 +1,8 @@
import type { Proforma } from "../../shared";
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.
*
@ -27,5 +29,7 @@ export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpd
globalDiscountPercentage: proforma.globalDiscountPercentage ?? 0,
paymentMethod: proforma.paymentMethod ?? "",
items: proforma.items.map(mapProformaItemsToProformaItemsUpdateForm),
};
};

View File

@ -9,12 +9,9 @@ import type { UpdateProformaByIdParams } from "../../shared";
import type { Proforma } from "../../shared/entities";
import { useProformaGetQuery, useProformaUpdateMutation } from "../../shared/hooks";
import { mapProformaToProformaUpdateForm, mapProformaToSelectedCustomer } from "../adapters";
import { type ProformaUpdateForm, ProformaUpdateFormSchema } from "../entities";
import {
type ProformaUpdateForm,
ProformaUpdateFormSchema,
defaultProformaUpdateForm,
} from "../entities";
import {
buildProformaUpdateDefault,
buildProformaUpdatePatch,
buildUpdateProformaByIdParams,
focusFirstProformaUpdateError,
@ -54,7 +51,7 @@ export const useUpdateProformaController = (
} = useProformaUpdateMutation();
const initialValues = useMemo<ProformaUpdateForm>(() => {
if (!proformaData) return defaultProformaUpdateForm;
if (!proformaData) return buildProformaUpdateDefault();
return mapProformaToProformaUpdateForm(proformaData);
}, [proformaData]);
@ -84,7 +81,7 @@ export const useUpdateProformaController = (
const resetForm = () => {
const initialData = proformaData
? mapProformaToProformaUpdateForm(proformaData)
: defaultProformaUpdateForm;
: buildProformaUpdateDefault();
form.reset(initialData, { keepDirty: false });
setSelectedCustomer(mapProformaToSelectedCustomer(proformaData));
@ -120,6 +117,9 @@ export const useUpdateProformaController = (
const previousData = proformaData;
const patchData = buildProformaUpdatePatch(formData, form.formState.dirtyFields);
console.log(patchData);
const params = buildUpdateProformaByIdParams(proformaId, patchData);
try {
@ -147,7 +147,9 @@ export const useUpdateProformaController = (
error instanceof Error ? error : new Error(t("pages.update.error.unknown"));
form.reset(
previousData ? mapProformaToProformaUpdateForm(previousData) : defaultProformaUpdateForm,
previousData
? mapProformaToProformaUpdateForm(previousData)
: buildProformaUpdateDefault(),
{ keepDirty: false }
);
@ -159,6 +161,7 @@ export const useUpdateProformaController = (
}
},
(errors: FieldErrors<ProformaUpdateForm>) => {
console.log(errors);
focusFirstProformaUpdateError(errors);
showWarningToast(

View File

@ -1,3 +1,4 @@
import { NumberHelper } from "@erp/core";
import * as React from "react";
import {
type FieldArrayWithId,
@ -7,11 +8,11 @@ import {
} from "react-hook-form";
import type { ProformaItemUpdateForm, ProformaUpdateForm } from "../entities";
import { buildProformaItemUpdateDefault } from "../utils/build-proforma-item-update-default";
import { buildProformaItemUpdateDefault } from "../utils";
export interface ProformaItemAmounts {
subtotal: number;
discountAmount: number;
itemDiscountAmount: number;
total: number;
}
@ -56,23 +57,27 @@ const normalizeItemPositions = (items: ProformaItemUpdateForm[]): ProformaItemUp
};
const calculateItemAmounts = (
item?: Pick<ProformaItemUpdateForm, "quantity" | "unitAmount" | "discountPercentage">
item?: Pick<ProformaItemUpdateForm, "quantity" | "unitAmount" | "itemDiscountPercentage">
): ProformaItemAmounts => {
if (!item) {
return {
subtotal: 0,
discountAmount: 0,
itemDiscountAmount: 0,
total: 0,
};
}
const subtotal = roundCurrency(item.quantity * item.unitAmount);
const discountAmount = roundCurrency(subtotal * (item.discountPercentage / 100));
const total = roundCurrency(subtotal - discountAmount);
const quantity = NumberHelper.toSafeNumber(item.quantity);
const unitAmount = NumberHelper.toSafeNumber(item.unitAmount);
const itemDiscountPercentage = NumberHelper.toSafeNumber(item.itemDiscountPercentage);
const subtotal = roundCurrency(quantity * unitAmount);
const itemDiscountAmount = roundCurrency(subtotal * (itemDiscountPercentage / 100));
const total = roundCurrency(subtotal - itemDiscountAmount);
return {
subtotal,
discountAmount,
itemDiscountAmount,
total,
};
};
@ -84,7 +89,7 @@ const calculateItemsTotals = (items: ProformaItemUpdateForm[]): ProformaItemsTot
return {
subtotal: roundCurrency(acc.subtotal + amounts.subtotal),
discountAmount: roundCurrency(acc.discountAmount + amounts.discountAmount),
discountAmount: roundCurrency(acc.discountAmount + amounts.itemDiscountAmount),
total: roundCurrency(acc.total + amounts.total),
};
},

View File

@ -1,5 +1,6 @@
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.schema";
export * from "./proforma-update-form-default.entity";
export * from "./proforma-update-patch.entity";

View File

@ -2,7 +2,7 @@ export interface ProformaItemUpdateForm {
id: string;
position: number;
description: string;
quantity: number;
unitAmount: number;
discountPercentage: number;
quantity: number | null;
unitAmount: number | null;
itemDiscountPercentage: number | null;
}

View File

@ -17,10 +17,11 @@ import { z } from "zod/v4";
export const ProformaItemUpdateFormSchema = z.object({
id: z.uuid(),
position: z.number().int().nonnegative(),
description: z.string().trim(),
quantity: z.number().positive(),
unitAmount: z.number().nonnegative(),
discountPercentage: z.number().min(0).max(100),
quantity: z.number().positive().nullable(),
unitAmount: z.number().nonnegative().nullable(),
itemDiscountPercentage: z.number().min(0).max(100).nullable(),
});
export type ProformaItemUpdateForm = z.infer<typeof ProformaItemUpdateFormSchema>;
export type ProformaItemUpdateFormSchemaType = z.infer<typeof ProformaItemUpdateFormSchema>;

View File

@ -0,0 +1,8 @@
export interface ProformaItemUpdatePatch {
id: string;
position: number;
description: string;
quantity: number | null;
unitAmount: number | null;
itemDiscountPercentage: number | null;
}

View File

@ -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: [],
};

View File

@ -13,7 +13,7 @@
* - 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 {
series: string;

View File

@ -42,7 +42,7 @@ export const ProformaUpdateEditorForm = ({
const { t } = useTranslation();
return (
<form className="space-y-6" id={formId} onSubmit={onSubmit}>
<form className="space-y-6" id={formId} noValidate onSubmit={onSubmit}>
<ProformaUpdateHeaderEditor disabled={isSubmitting} />
<ProformaUpdateRecipientEditor

View File

@ -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 { CopyIcon, MoveDownIcon, MoveUpIcon, Trash2Icon } from "lucide-react";
@ -38,28 +38,24 @@ export const ProformaUpdateItemRowEditor = ({
className="col-span-12"
label={t("form_fields.items.description.label", "Descripción")}
name={`items.${index}.description`}
required
/>
<TextField
<QuantityField
className="col-span-12 md:col-span-3"
label={t("form_fields.items.quantity.label", "Cantidad")}
name={`items.${index}.quantity`}
typePreset="number"
/>
<TextField
<AmountField
className="col-span-12 md:col-span-3"
label={t("form_fields.items.unit_amount.label", "Importe unitario")}
name={`items.${index}.unitAmount`}
typePreset="number"
/>
<TextField
<PercentageField
className="col-span-12 md:col-span-3"
label={t("form_fields.items.discount_percentage.label", "Descuento %")}
name={`items.${index}.discountPercentage`}
typePreset="number"
/>
<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">
<span>{t("form_fields.items.discount_amount.label", "Descuento")}</span>
<span>{amounts.discountAmount}</span>
<span>{amounts.itemDiscountAmount}</span>
</div>
<div className="flex justify-between gap-3 font-medium">

View File

@ -7,8 +7,8 @@ export const buildProformaItemUpdateDefault = (position: number): ProformaItemUp
id: UniqueID.generateNewID().toString(),
position,
description: "",
quantity: 1,
unitAmount: 0,
discountPercentage: 0,
quantity: null,
unitAmount: null,
itemDiscountPercentage: null,
};
};

View File

@ -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,
}));
};

View File

@ -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: [],
};
};

View File

@ -3,6 +3,8 @@ import type { FieldNamesMarkedBoolean } from "react-hook-form";
import type { ProformaUpdateForm, ProformaUpdatePatch } from "../entities";
import { buildProformaItemsUpdatePatch } from "./build-proforma-items-update-patch";
export const buildProformaUpdatePatch = (
formData: ProformaUpdateForm,
dirtyFields: FieldNamesMarkedBoolean<ProformaUpdateForm>
@ -11,5 +13,10 @@ export const buildProformaUpdatePatch = (
return {};
}
return pickFormDirtyValues(formData, dirtyFields) as ProformaUpdatePatch;
const itemsPatch = buildProformaItemsUpdatePatch(formData.items);
return {
...pickFormDirtyValues(formData, dirtyFields),
items: itemsPatch,
} as ProformaUpdatePatch;
};

View File

@ -1,4 +1,5 @@
export * from "./build-proforma-item-update-default";
export * from "./build-proforma-update-default";
export * from "./build-proforma-update-patch";
export * from "./build-update-proforma-by-id-params";
export * from "./focus-first-proforma-update-error";

View File

@ -17,7 +17,7 @@ export const CustomerUpdateEditorForm = ({
className,
}: CustomerUpdateEditorFormProps) => {
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)}>
<CustomerBasicInfoFields />
<CustomerAddressFields />

View File

@ -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>
);
};

View File

@ -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;
};

View File

@ -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}`;
};

View File

@ -0,0 +1,2 @@
export * from "./decimal-field.tsx";
export * from "./decimal-field.types.ts";

View File

@ -1,8 +1,10 @@
export * from "./date-picker-field.tsx";
export * from "./date-picker-input-field/index.ts";
export * from "./decimal-field/index.ts";
export * from "./form-field-label.tsx";
export * from "./multi-select-field.tsx";
export * from "./radio-group-field.tsx";
export * from "./select-field.tsx";
export * from "./semantic-numeric-fields/index.ts";
export * from "./text-area-field.tsx";
export * from "./text-field.tsx";

View File

@ -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} />;
};

View File

@ -0,0 +1,3 @@
export * from "./amount-field.tsx";
export * from "./percentage-field.tsx";
export * from "./quantity-field.tsx";

View File

@ -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} />;
};

View File

@ -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} />;
};

View File

@ -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>;
};

View File

@ -63,7 +63,7 @@ export const getInputPresetProps = (preset: TextFieldTypePreset = "text"): Resol
case "number":
return {
type: "text",
type: "number",
inputMode: "numeric",
autoComplete: "off",
spellCheck: false,